#include "pch.hpp"
#include "System.hpp"
#include "NestedState.hpp"
#include "Parser.hpp"

#include <engine.core/File.hpp>

using namespace Engine;
using namespace Engine::Ui;




struct vs_globals
{
	float      screenWidth;
	float      screenHeight;
	float      texWidthInv;
	float      texHeightInv;

	static StaticReflection reflect()
	{
		static const StaticReflectionElement table[] = {
			{ "screenWidth",  0, Format::R32_FLOAT, 0 },
			{ "screenHeight", 0, Format::R32_FLOAT, 0 },
			{ "texWidthInv",  0, Format::R32_FLOAT, 0 },
			{ "texHeightInv", 0, Format::R32_FLOAT, 0 },
			{ 0 },
		};

		BOOST_STATIC_ASSERT(sizeof(vs_globals) == 16);

		return StaticReflection(table, sizeof(vs_globals));
	}
};
///////////////////////////////////////////////////////////////////////////////////////////////////

struct ps_globals
{
	float4   wireframeColor;

	static StaticReflection reflect()
	{
		static const StaticReflectionElement table[] =
		{
			{ "wireframeColor", 0, Format::R32G32B32A32_FLOAT, 0 },
			0
		};

		return StaticReflection(table, sizeof(ps_globals));
	}
};
///////////////////////////////////////////////////////////////////////////////////////////////////




Ui::System::System(const EngineInfo& info, Video::Device::ptr videoDevice, Input::Device::ptr inputDevice)
	: m_logger(info.serviceProvider->service<logger::stream>())
	, m_videoDevice(videoDevice)
	, m_inputDevice(inputDevice)
	, m_mesh(videoDevice->createMesh(GuiVertices(), Indices16(), Video::Primitive::IndexedTriangles, Video::Mesh::Dynamic))

	, m_effect(createEffect())
	, m_style(createStyle(":/ui/default"))
	, m_texture(createTexture(":/ui/default.png"))

	, m_desktop(Desktop::create(int2(0, 0), videoDevice->size(), m_style))
	, m_state(SystemState::create(Cursor(inputDevice), m_desktop))

	, m_wireframe(false)
	, m_normalNode(createNormalNode())
	, m_wireframeNode(createWireframeNode())
{
	m_desktop->updateStyle();
}
///////////////////////////////////////////////////////////////////////////////////////////////////




QString Ui::System::name() const
{
	return "UiSystem";
}
///////////////////////////////////////////////////////////////////////////////////////////////////




void Ui::System::loadStyle(QString style, QString texture)
{
	setStyle(createStyle(style), createTexture(texture));
}
///////////////////////////////////////////////////////////////////////////////////////////////////

void Ui::System::setStyle(BasicStyleDefinition::ptr style, Video::Texture::ptr texture)
{
	// The style may not be able to load (for whatever reason)
	// We want the system to keep the old style, no matter what
	// Hence the following code
	bool commit = false;

	auto oldStyle = m_style;
	auto oldTexture = m_texture;

	try
	{
		changeStyle(style, texture);
	}
	catch(critical_exception&)
	{
		// A critical exception shall always trigger a shutdown
		throw;
	}
	catch(exception&)
	{
		// Somehow loading the new font didn't work: So we perform a rollback

		try
		{
			// Tell all widgets to use the old style.
			// When an exception has been thrown, some widgets could be
			// in an undefined state (some might use new glyphs, others
			// old ones). We thus tell all widgets to use the current style
			// to get a defined result at all costs.
			changeStyle(oldStyle, oldTexture);
		}
		catch(exception& e)
		{
			throw critical_exception(format("Unable to perform style rollback: %1%") % e.what()) << debug_info_e(e);
		}
	}
}
///////////////////////////////////////////////////////////////////////////////////////////////////

bool Ui::System::wireframe() const
{
	return m_wireframe;
}
///////////////////////////////////////////////////////////////////////////////////////////////////

void Ui::System::setWireframe(bool b)
{
	m_wireframe = b;
}
///////////////////////////////////////////////////////////////////////////////////////////////////

Desktop::ptr Ui::System::desktop() const
{
	return m_desktop;
}
///////////////////////////////////////////////////////////////////////////////////////////////////

SystemState::ptr Ui::System::state() const
{
	return m_state;
}
///////////////////////////////////////////////////////////////////////////////////////////////////




void Ui::System::update()
{
	//
	// Update
	//

	// Important Functions:
	//   IDirect3DDevice9::SetCursorProperties
	//   IDirect3DDevice9::ShowCursor
	//   IDirect3DDevice9::SetCursorPosition

	m_state->moveAttached();
	m_state->resizeAttached();


	// Create a nested state that simply wraps around our own state
	// changes to that instance will modify our state...
	// The first special thing is that we only update the modal widget and it's children

	NestedState state(m_state);
	m_state->modalWidget()->hitTest(state);
	m_state->modalWidget()->update(state);

	//
	// Rendering
	//

	render();
}
///////////////////////////////////////////////////////////////////////////////////////////////////

void Ui::System::processInput(QChar c)
{
	// Process user input
	// Only the currently focused widget should receive such input
	Widget::ptr focused = m_state->focusWidget();
	if(focused)
	{
		focused->processInput(c);
	}
}
///////////////////////////////////////////////////////////////////////////////////////////////////

void Ui::System::resize(uint32 width, uint32 height)
{
	// We will resize the desktop to match the entire screen
	m_desktop->resize(intsize2(width, height));

	vs_globals vs;
	vs.screenWidth  = m_videoDevice->width();
	vs.screenHeight = m_videoDevice->height();
	vs.texWidthInv  = 1.0f / m_style->size().x;
	vs.texHeightInv = 1.0f / m_style->size().y;

	// Update the pass of both nodes (the screensize has changed, thus we need to update the
	// shader parameters)
	{
		auto pass = m_normalNode->passes.front();
		pass.vertexShader.constants["$Globals"]->update(vs);
	}

	{
		auto pass = m_wireframeNode->passes.front();
		pass.vertexShader.constants["$Globals"]->update(vs);
	}

	m_logger->log_debug(QString("Successfully resized to %1x%2 px").arg(width).arg(height));
}
///////////////////////////////////////////////////////////////////////////////////////////////////




void Ui::System::render()
{
	// Calculate the number of quads and vertices
	// for our new buffer: required to decide if we use
	// a 16 or 32bit indexbuffer
	uint32 numQuads = m_desktop->numQuads();
	uint32 numVertices = Quad::numVertices(numQuads);

	// We resize our vector of quads in one go:
	// we don't wanna waist time on incremental allocation and copying
	m_quads.clear();
	m_quads.reserve(numQuads);
	m_vertices.clear();
	m_vertices.reserve(numVertices);


	// Now we do the rendering...
	// Contrary to the update, everything is being rendered
	// (instead of only the modal widget)
	NestedState state(m_state);
	m_desktop->render(m_quads, state);

	// Next we have to decide if a 16bit buffer is enough, or if we have to switch to a 32bit one..
	// (There should never be so much indices, but we'll see...
	Quad::vertices(m_quads, m_vertices);
	if(numVertices <= std::numeric_limits<uint16>::max())
	{
		m_indices32.swap(Indices32());

		// 16bit indices are sufficient enough: There are only
		// 2^16 vertices in the buffer
		m_indices16.clear();
		Quad::indices(m_indices16, numQuads);
		m_mesh->update(m_vertices, m_indices16);
	}
	else
	{
		m_indices16.swap(Indices16());

		// 16bit indices are not sufficient enough: There are more
		// than 2^16 vertices in the buffer
		m_indices32.clear();
		Quad::indices(m_indices32, numQuads);
		m_mesh->update(m_vertices, m_indices32);
	}

	// The mesh has been properly updated..
	// It can be sent to the videoDevice for
	// rendering
	m_videoDevice->render(m_normalNode);

	if(m_wireframe)
		m_videoDevice->render(m_wireframeNode);
}
///////////////////////////////////////////////////////////////////////////////////////////////////




Video::RenderNode::ptr Ui::System::createNormalNode() const
{
	auto normalNode = Video::RenderNode::create(m_effect);
	normalNode->distance = -1;
	normalNode->technique = "RenderUi";

	vs_globals vs;
	vs.screenWidth  = m_videoDevice->width();
	vs.screenHeight = m_videoDevice->height();
	vs.texWidthInv  = 1.0f / m_style->size().x;
	vs.texHeightInv = 1.0f / m_style->size().y;

	auto sampler = m_videoDevice->createSamplerState();
	sampler->setFilter(Video::Filter::MinMagMipPoint);
	sampler->setAddressU(Video::Address::Border);
	sampler->setAddressV(Video::Address::Border);
	sampler->setAddressW(Video::Address::Border);
	sampler->setBorderColor(float4(0, 1, 0, 1));

	auto blendState = m_videoDevice->createBlendState();
	blendState->setSrcBlend(Video::Blend::SrcAlpha);
	blendState->setDestBlend(Video::Blend::InvSrcAlpha);
	blendState->setBlendOp(Video::BlendOp::Add);

	auto rasterizerState = m_videoDevice->createRasterizerState();
	rasterizerState->setDepthclipping(false);
	rasterizerState->setFillmode(Video::Fillmode::solid);
	rasterizerState->setCullmode(Video::Cullmode::none);

	Video::RenderPass pass(m_mesh);
	pass.name = "P0";
	pass.vertexShader.constants["$Globals"] = m_videoDevice->createConstantsBuffer(vs);
	pass.pixelShader.textures["fontTexture"] = m_texture;
	pass.pixelShader.samplers["fontSampler"] = sampler;
	pass.blendState = blendState;
	pass.rasterizerState = rasterizerState;

	normalNode->addPass(pass);

	return normalNode;
}
///////////////////////////////////////////////////////////////////////////////////////////////////

Video::RenderNode::ptr Ui::System::createWireframeNode() const
{
	auto wireframeNode = Video::RenderNode::create(m_effect);
	wireframeNode->distance = -1;
	wireframeNode->technique = "RenderWireframe";

	vs_globals vs;
	vs.screenWidth  = m_videoDevice->width();
	vs.screenHeight = m_videoDevice->height();
	vs.texWidthInv  = 1.0f / m_style->size().x;
	vs.texHeightInv = 1.0f / m_style->size().y;

	ps_globals ps;
	ps.wireframeColor = float4(1, 0, 0, 1);

	Video::RenderPass pass(m_mesh);
	pass.name = "P0";
	pass.vertexShader.constants["$Globals"] = m_videoDevice->createConstantsBuffer(vs);
	pass.pixelShader.constants["$Globals"]  = m_videoDevice->createConstantsBuffer(ps);
	pass.rasterizerState = m_videoDevice->createRasterizerState();
	pass.rasterizerState->setDepthclipping(false);
	pass.rasterizerState->setFillmode(Video::Fillmode::wireframe);
	pass.rasterizerState->setCullmode(Video::Cullmode::none);

	wireframeNode->addPass(pass);

	return wireframeNode;
}
///////////////////////////////////////////////////////////////////////////////////////////////////

Video::Effect::ptr Ui::System::createEffect() const
{
	auto effect = m_videoDevice->createEffect("ui_effect");

	QByteArray vertexData = File::readAll(":/effects/vs");
	QByteArray pixelData  = File::readAll(":/effects/ps");
	QByteArray pixelWfData = File::readAll(":/effects/ps_wf");

	{
		Video::EffectTechnique& t1 = effect->addTechnique("RenderUi");
		Video::EffectPass& p0 = t1.addPass("P0");
		p0.addVertexShader(vertexData, "main", GuiVertex::reflect());
		p0.addPixelShader(pixelData, "main");
	}

	{
		Video::EffectTechnique& t2 = effect->addTechnique("RenderWireframe");
		Video::EffectPass& p0 = t2.addPass("P0");
		p0.addVertexShader(vertexData, "main", GuiVertex::reflect());
		p0.addPixelShader(pixelWfData, "main");
	}

	effect->optimize();

	return effect;
}
///////////////////////////////////////////////////////////////////////////////////////////////////

BasicStyleDefinition::ptr Ui::System::createStyle(QString fname) const
{
	BasicStyleDefinition::ptr style(new BasicStyleDefinition());
	style->load<Parser>(fname);
	return style;
}
///////////////////////////////////////////////////////////////////////////////////////////////////

Video::Texture::ptr Ui::System::createTexture(QString fname) const
{
	return m_videoDevice->createTexture(fname);
}
///////////////////////////////////////////////////////////////////////////////////////////////////




void Ui::System::changeStyle(BasicStyleDefinition::ptr style, Video::Texture::ptr texture)
{
	// Actual style loading routine...
	// We must prepare a path to load the texture from, as the one from the style
	// document is relative to the document's path
	m_style         = style;
	m_texture       = texture;
	m_normalNode    = createNormalNode();
	m_wireframeNode = createWireframeNode();

	// We try to load the new style, but that might not work, because
	// for example a glyph or font is missing.
	m_desktop->setStyleDefinition(m_style);
}
///////////////////////////////////////////////////////////////////////////////////////////////////
